ItIron2023
react
我們昨天做了一個基本的井字遊戲,基本上除了css的部分有些困難之外,基本的邏輯實踐其實相當的單純,今天我們反其道而行,雖然一樣是做個小遊戲,但我們今天把重點放在邏輯的處理上,css的部分不用你煩惱!
請你以這個codesandbox作為起頭並參考下方的instructions完成題目的要求。
請你打造一個滿足以下條件的記憶卡遊戲
最終期待的成果如下圖
以下為基礎的starter code
const App = () => {
const initialCards = ['A', 'A', 'B', 'B', 'C', 'C', 'D', 'D', 'E', 'E', 'F', 'F'];
const [cards, setCards] = useState(initialCards);
const [flipped, setFlipped] = useState(Array(12).fill(false));
const [check, setCheck] = useState([]);
const [completed, setCompleted] = useState([]);
// Shuffle cards on mount
useEffect(() => {
setCards(initialCards.sort(() => Math.random() - 0.5));
}, []);
const handleFlip = (index) => {
// TODO: Implement handleFlip
};
// TODO: Implement the logic to check for matching cards
// TODO: Implement the logic to display the game status
const gameStatus = "";
return (
<div className="container">
{cards.map((card, index) => (
<div className="card-container" key={index} onClick={() => handleFlip(index)}>
<div className={`card ${flipped[index] || completed.includes(index) ? 'flip' : ''}`}>
<div className={`front ${completed.includes(index) ? 'matched' : ''}`}></div>
<div className={`back ${completed.includes(index) ? 'matched' : ''}`}>{card}</div>
</div>
</div>
))}
{gameStatus}
</div>
);
};
export default App;
.container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 16px;
margin: 20px;
}
.card-container {
width: 100px;
height: 100px;
position: relative;
perspective: 1000px;
margin: 10px;
display: inline-block;
}
.card {
width: 100%;
height: 100%;
position: absolute;
transform-style: preserve-3d;
transition: transform 0.5s;
}
.card.flip {
transform: rotateY(180deg);
}
.card .front,
.card .back {
width: 100%;
height: 100%;
position: absolute;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: #ccc;
}
.card .back {
transform: rotateY(180deg);
background-color: #4caf50;
color: white;
}
.matched {
background-color: #ffd700;
color: black;
}
這個題目稍稍有些難度,但我仍認為最難的地方在於css的處理上,好在這次題目我先替你處理完這部分了,有興趣的朋友可以觀察一下我是如何實踐翻轉的動畫,大致上的重點在於以下幾個屬性的使用
1. perspective: 1000px; => 讓3D效果足夠明顯但又不會太誇張
2. transform-style: preserve-3d; => 讓子層的front & back可以被定位在3D平面上
3. backface-visibility: hidden; => 讓卡片的一面不會同時顯示到另一面上
4. transform: rotateY(180deg); => 讓卡片以Y軸做180旋轉來實現翻轉效果或是藏住背面卡片的內容
有興趣的可以自行研究一下,雖然很有趣但並不是今天的重點。
我們首先來觀察一下題目的給你的幾個state,當然你可以隨意修改,不過你也是可以用既有的state完成題目的要求。
// 儲存一開始的12張卡,其中包含六個對子
const [cards, setCards] = useState(initialCards);
// 儲存目前每張卡是否翻開的狀態,用以決定下方是否要掛上對應的class
const [flipped, setFlipped] = useState(Array(12).fill(false));
// 儲存目前正在翻的卡片,當翻到兩張時開始檢查的相關邏輯
const [check, setCheck] = useState([]);
// 儲存所有已經成對的卡片,用以決定下方是否要掛上對應的class
const [completed, setCompleted] = useState([]);
其中所有的卡片我們都會以index作為紀錄,辨別目前該修改哪張卡片的狀態,理解這一點之後我們就可以先處理最重要的handleFlip函數了,內容相當的單純
const handleFlip = (index) => {
if (flipped[index] || completed.includes(index)) return;
setFlipped((prev) => {
const copy = [...prev];
copy[index] = true;
return copy;
});
setCheck((prev) => [...prev, index]);
};
接著我們要處理check的部分,每一次以兩張卡為限,每當翻開兩張時我們就要檢查是否兩張卡有成對,這邊有許多種做法,我提供其中一種最為直觀的解法,也就是利用一個useEffect去處理這個邏輯。
1. 先檢查是否目前已翻開兩張卡
2. 若已經翻開兩張卡,則確認是否成對並利用setCompleted更新狀態
3. 若沒有成對,則在半秒後將兩張卡利用setFlipped蓋上
4. 利用setCheck設為初始狀態
useEffect(() => {
if (check.length === 2) {
const [first, second] = check;
if (cards[first] === cards[second]) {
setCompleted([...completed, first, second]);
} else {
setTimeout(() => {
setFlipped((prev) => {
const copy = [...prev];
copy[first] = false;
copy[second] = false;
return copy;
});
}, 500);
}
setCheck([]);
}
}, [check]);
最後的部分就簡單多了,我們只要檢查completed的長度是否跟初始陣列一樣長就可以判斷遊戲是否結束囉。
const gameStatus = completed.length === 12 ? "You Win!" : "";
今天我們處理了一個翻牌遊戲的問題,在沒有先提供css支援以及starter code的情況下,要在面試的30分鐘內做完會相當有難度,不過仍不失為一個有趣的挑戰題目,整體上並沒有太過於複雜的邏輯,完全可以靠useState & useEffect兩個hook就達成題目的要求,我想你也慢慢了解為什麼我們前面花這麼多時間探討這兩個hook的相關錯誤了,他們就是最常被使用的。整個系列文章就到此為止,希望透過這30天的練習有讓你對react更有自信一些,面對相關的面試時也能更如魚得水一些!我們明年見囉!
本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!